from collections import Counter
from operator import itemgetter
from functools import reduce
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.offline as pyo
from mongoengine import connect
import src.settings as settings
from src.visualization.statistics import *
from src.features.preprocessing import convert_salary
from src.data.vacancy import Vacancy
connect(
host=settings.db_host,
port=settings.db_port,
db=settings.db_name
)
MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True, read_preference=Primary())
pyo.init_notebook_mode()
% load_ext autoreload
% autoreload 2
The autoreload extension is already loaded. To reload it, use: %reload_ext autoreload
df: pd.DataFrame = (
Vacancy
.objects
.to_dataframe(include=[
'_id',
'name',
'description',
'salary',
'schedule.name',
'experience',
'employment.name',
'area.name',
'address.lat',
'address.lng',
'address.city',
'specializations',
'employer.name',
'professional_roles',
'key_skills',
])
)
df.set_index('_id', inplace=True)
cp = df.copy()
df = cp.copy()
df['salary.to'].fillna(df['salary.from'], inplace=True)
df = df[df['salary.from'].notna()]
df = df[df['salary.to'].notna()]
df = df[df['salary.currency'].notna()]
df['salary.currency'].isna().sum()
0
df.shape
(47440, 18)
df[['salary.from', 'salary.to', 'salary.currency']] = df[['salary.from', 'salary.to', 'salary.currency']].apply(
lambda row: [
convert_salary(row['salary.from'], from_currency=row['salary.currency'], db=settings.db),
convert_salary(row['salary.to'], from_currency=row['salary.currency'], db=settings.db),
row['salary.currency']
], axis=1, result_type='expand')
df['mean_salary'] = np.round((df['salary.to'] + df['salary.from']) / 2)
df[df['salary.currency'] != 'RUR'].head(10)
| description | key_skills | schedule.name | experience.id | experience.name | employment.name | salary.to | salary.from | salary.currency | salary.gross | name | area.name | employer.name | specializations | professional_roles | address.city | address.lat | address.lng | mean_salary | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| _id | |||||||||||||||||||
| 49810443 | Требуемый опыт работы: 1–3 года Частичная заня... | [Internet, Голландский язык, Работа в команде,... | Удаленная работа | between1And3 | От 1 года до 3 лет | Частичная занятость | 84674.005080 | 16934.801016 | EUR | True | Преподаватель голландского языка (онлайн) | Алматы | Lingolands | [{'id': '14.60', 'name': 'Гуманитарные науки',... | [{'id': '132', 'name': 'Учитель, преподаватель... | NaN | NaN | NaN | 50804.403048 |
| 49226918 | Сеть магазинов "Соседи" приглашает на работу з... | [Управление персоналом, Пользователь ПК, Работ... | Сменный график | between1And3 | От 1 года до 3 лет | Полная занятость | 37745.710055 | 25550.942191 | BYR | False | Заведующий отделом розничных продаж | Смолевичи | СОСЕДИ, Сеть магазинов | [{'id': '17.324', 'name': 'Управление продажам... | [{'id': '127', 'name': 'Товаровед'}] | NaN | NaN | NaN | 31648.326123 |
| 49225403 | Обязанности: ручная бережная и качественная м... | [] | Полный день | noExperience | Нет опыта | Полная занятость | 29035.161581 | 29035.161581 | BYR | False | Мойщик автомобилей | Минск | Такси Алмаз 7788 | [{'id': '15.390', 'name': 'Автомобильный бизне... | [{'id': '4', 'name': 'Автомойщик'}] | Минск | 53.899117 | 27.524919 | 29035.161581 |
| 49225631 | Приглашаем водителей для работы в "Такси Алмаз... | [] | Гибкий график | noExperience | Нет опыта | Частичная занятость | 43552.742371 | 43552.742371 | BYR | False | Водитель Такси Алмаз 7788 | Минск | Такси Алмаз 7788 | [{'id': '21.482', 'name': 'Водитель', 'profare... | [{'id': '21', 'name': 'Водитель'}] | Минск | 53.899117 | 27.524919 | 43552.742371 |
| 50155391 | Обязанности: - Обработка входящего потока сооб... | [Грамотная речь, Пользователь ПК, Работа в ком... | Удаленная работа | noExperience | Нет опыта | Полная занятость | 44716.053063 | 25339.096736 | USD | False | Менеджер по продажам в мессенджерах (Direct-ме... | Киев | Миронов Владимир | [{'id': '17.149', 'name': 'Менеджер по работе ... | [{'id': '54', 'name': 'Координатор отдела прод... | NaN | NaN | NaN | 35027.574899 |
| 50157216 | Please note that by applying to this vacancy y... | [Английский язык, Coaching, Leadership Skills,... | Полный день | noExperience | Нет опыта | Стажировка | 63405.792445 | 63405.792445 | KZT | True | Sales Intern/ Стажер в отдел продаж | Алматы | Procter & Gamble | [{'id': '15.389', 'name': 'Продажи', 'profarea... | [{'id': '40', 'name': 'Другое'}] | NaN | NaN | NaN | 63405.792445 |
| 50155707 | Обязанности: Запуск рекламных компаний Google,... | [Английский язык, Маркетинговый анализ, Подгот... | Полный день | between1And3 | От 1 года до 3 лет | Полная занятость | 260843.642868 | 111790.132658 | USD | False | PPC specialist / Специалист по контекстной рек... | Москва | WeFix Appliance Repair | [{'id': '1.246', 'name': 'Развитие бизнеса', '... | [{'id': '68', 'name': 'Менеджер по маркетингу ... | NaN | NaN | NaN | 186316.887763 |
| 50156232 | Makeomatic расширяет свою команду! Уже 7 лет м... | [Git, JavaScript, Node.js, Docker, GitHub, Dev... | Полный день | between3And6 | От 3 до 6 лет | Полная занятость | 335370.397973 | 260843.642868 | USD | True | Senior Backend Developer (Node.js) | Москва | Makeomatic Inc | [{'id': '1.221', 'name': 'Программирование, Ра... | [{'id': '96', 'name': 'Программист, разработчи... | NaN | NaN | NaN | 298107.020420 |
| 50157118 | Топ-менеджер/Руководитель/Управление (банковск... | [Работа в команде, CRM, Телефонные переговоры,... | Удаленная работа | between1And3 | От 1 года до 3 лет | Полная занятость | 335370.397973 | 111790.132658 | USD | False | Руководитель (Управление, банковская сфера) | Владивосток | Красный Джин | [{'id': '9.226', 'name': 'Продажи', 'profarea_... | [{'id': '40', 'name': 'Другое'}] | NaN | NaN | NaN | 223580.265315 |
| 50157432 | Julia Valler Event Staffing is a high-end mode... | [Английский язык, Работа в команде, MS PowerPo... | Полный день | noExperience | Нет опыта | Полная занятость | 74526.755105 | 74526.755105 | USD | True | Sales Manager (Full Time Remote) | США | Julia Valler Staffing | [{'id': '17.242', 'name': 'Прямые продажи', 'p... | [{'id': '70', 'name': 'Менеджер по продажам, м... | NaN | NaN | NaN | 74526.755105 |
df.head(10)
| description | key_skills | schedule.name | experience.id | experience.name | employment.name | salary.to | salary.from | salary.currency | salary.gross | name | area.name | employer.name | specializations | professional_roles | address.city | address.lat | address.lng | mean_salary | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| _id | |||||||||||||||||||
| 49810439 | Обязанности: Своевременная подача автомобиля; ... | [] | Полный день | between3And6 | От 3 до 6 лет | Полная занятость | 80000.0 | 60000.0 | RUR | False | Водитель в семью | Москва | Антонова Анастасия | [{'id': '21.17', 'name': 'Автоперевозки', 'pro... | [{'id': '21', 'name': 'Водитель'}] | NaN | NaN | NaN | 70000.0 |
| 49810551 | Обязанности: Уборка дома 500 кв.м., стирка, г... | [Русский язык, Чистоплотность] | Полный день | between1And3 | От 1 года до 3 лет | Полная занятость | 105000.0 | 100000.0 | RUR | False | Помощник по хозяйству на дачу | Санкт-Петербург | Агентство Прайм Домашний Персонал | [{'id': '4.494', 'name': 'Уборщица/уборщик', '... | [{'id': '130', 'name': 'Уборщица, уборщик'}] | Санкт-Петербург | 59.932428 | 30.439198 | 102500.0 |
| 49810468 | Студия Красоты и здоровья Кристалл ищет парикм... | [Пользователь ПК, Работа в команде, Грамотная ... | Полный день | between1And3 | От 1 года до 3 лет | Полная занятость | 25000.0 | 25000.0 | RUR | True | Парикмахер-универсал | Волгоград | Кристалл | [{'id': '24.493', 'name': 'Парикмахер', 'profa... | [{'id': '92', 'name': 'Парикмахер'}] | NaN | NaN | NaN | 25000.0 |
| 45788942 | Условия: ЗП от 50 тысяч на руки (оклад 22 тыся... | [Складская логистика, Терминалы Сбора Данных, ... | Сменный график | noExperience | Нет опыта | Полная занятость | 80000.0 | 50000.0 | RUR | False | Кладовщик - комплектовщик | Тула | Симпл Деливери Груп | [{'id': '21.563', 'name': 'Кладовщик', 'profar... | [{'id': '131', 'name': 'Упаковщик, комплектовщ... | рабочий посёлок Горки Ленинские | 55.520630 | 37.774149 | 65000.0 |
| 49810601 | Уважаемые соискатели, рассматриваются кандидат... | [] | Полный день | moreThan6 | Более 6 лет | Полная занятость | 100000.0 | 100000.0 | RUR | False | Заместитель главного бухгалтера (производство) | Ростов-на-Дону | АнРуссТранс | [{'id': '2.335', 'name': 'Учет заработной плат... | [{'id': '18', 'name': 'Бухгалтер'}] | NaN | NaN | NaN | 100000.0 |
| 49810507 | Логопедический Пункт 1 приглашает Администрато... | [Обучение персонала, Пользователь ПК, Организа... | Полный день | between1And3 | От 1 года до 3 лет | Полная занятость | 30000.0 | 25000.0 | RUR | True | Администратор детского центра | Волгоград | Логопедический Пункт №1 | [{'id': '4.332', 'name': 'Управляющий офисом (... | [{'id': '8', 'name': 'Администратор'}] | Волгоград | 48.745092 | 44.499916 | 27500.0 |
| 49810469 | Студия Красоты и здоровья Кристалл ищет парикм... | [Пользователь ПК, Работа в команде, Грамотная ... | Полный день | between1And3 | От 1 года до 3 лет | Полная занятость | 25000.0 | 25000.0 | RUR | True | Парикмахер-универсал | Волжский (Волгоградская область) | Кристалл | [{'id': '24.493', 'name': 'Парикмахер', 'profa... | [{'id': '92', 'name': 'Парикмахер'}] | NaN | NaN | NaN | 25000.0 |
| 49810426 | Обязанности: выполнение услуг массажа на высок... | [антицеллюлитный, класический, спортивный, лим... | Полный день | between3And6 | От 3 до 6 лет | Полная занятость | 150000.0 | 80000.0 | RUR | True | Массажистка/массажист | Москва | ЭК Брендинг | [{'id': '24.492', 'name': 'Массажист', 'profar... | [{'id': '60', 'name': 'Массажист'}] | Москва | 55.778796 | 37.598825 | 115000.0 |
| 47003369 | Медиахолдинг "Май Медиа" ищет менеджера по про... | [Прямые продажи, Телефонные переговоры, Навыки... | Полный день | between1And3 | От 1 года до 3 лет | Полная занятость | 70000.0 | 50000.0 | RUR | False | Ведущий клиентский менеджер | Иваново (Ивановская область) | Май Медиа | [{'id': '17.242', 'name': 'Прямые продажи', 'p... | [{'id': '105', 'name': 'Руководитель отдела кл... | Иваново | 57.001064 | 40.968217 | 60000.0 |
| 43592367 | Обязанности: Запрос цен и анализ по счетам от... | [MS PowerPoint, MS Access, Работа с базами дан... | Сменный график | noExperience | Нет опыта | Полная занятость | 31500.0 | 26250.0 | RUR | True | Оператор базы данных | Белгород | My Sky | [{'id': '2.33', 'name': 'Аудит', 'profarea_id'... | [{'id': '84', 'name': 'Оператор ПК, оператор б... | Белгород | 50.576507 | 36.578904 | 28875.0 |
total_salary = df['mean_salary'].sum()
total_salary
2782462963.154763
total_salary_by_area = df[['area.name', 'mean_salary']].groupby(['area.name'], as_index=False).sum().rename(
columns={'mean_salary': 'total_salary'})
total_salary_by_area.head(10)
| area.name | total_salary | |
|---|---|---|
| 0 | Абаза | 770000.0 |
| 1 | Абай | 86000.0 |
| 2 | Абакан | 8340594.0 |
| 3 | Абан | 119921.0 |
| 4 | Абатское | 195047.5 |
| 5 | Абинск | 65000.0 |
| 6 | Авсюнино | 131500.0 |
| 7 | Агалатово | 30000.0 |
| 8 | Агаповка | 86000.0 |
| 9 | Агеево | 66000.0 |
other = total_salary_by_area.total_salary < (total_salary / 100) # общая зарплата меньше 1%
other_value = total_salary_by_area.total_salary[other].agg('sum')
total_salary_by_area = total_salary_by_area[~other]
total_salary_by_area = total_salary_by_area.append({'area.name': 'Другие регионы', 'total_salary': other_value},
ignore_index=True)
total_salary_by_area
| area.name | total_salary | |
|---|---|---|
| 0 | Владивосток | 6.015153e+07 |
| 1 | Екатеринбург | 4.013486e+07 |
| 2 | Иркутск | 4.925897e+07 |
| 3 | Казань | 4.131740e+07 |
| 4 | Краснодар | 5.243045e+07 |
| 5 | Красноярск | 5.313064e+07 |
| 6 | Минск | 2.859788e+07 |
| 7 | Москва | 5.370685e+08 |
| 8 | Нижний Новгород | 3.684292e+07 |
| 9 | Новосибирск | 6.767413e+07 |
| 10 | Ростов-на-Дону | 3.439070e+07 |
| 11 | Санкт-Петербург | 2.051561e+08 |
| 12 | Уфа | 2.819119e+07 |
| 13 | Хабаровск | 4.851375e+07 |
| 14 | Другие регионы | 1.499604e+09 |
px.pie(total_salary_by_area, names='area.name', values='total_salary', title='Разделение зарплат по городам')
geo_df = df.reset_index()[['_id', 'address.city', 'address.lat', 'address.lng', 'mean_salary']]\
.groupby('address.city', as_index=False)\
.agg({'mean_salary': 'mean', '_id': 'count', 'address.lat': 'mean', 'address.lng': 'mean'})\
.rename(columns={'_id': 'count'})\
px.scatter_geo(
geo_df[(geo_df['count'] > 10) & (geo_df['mean_salary'] < 200_000)],
lat='address.lat',
lon='address.lng',
size='count',
fitbounds='locations',
color='mean_salary',
hover_data=['address.city'],
center={'lat': 53, 'lon': 83},
size_max=50,
labels={'mean_salary': 'Зарплата'},
title='Количество вакансий и средняя зарплата относительно города'
)
px.histogram(
df[df.mean_salary < 500_000],
x='mean_salary',
nbins=100,
title='Распределение зарплат',
labels={'mean_salary': 'Зарплата'}
)
px.histogram(
df[df.mean_salary < 500_000],
x='mean_salary',
color='schedule.name',
nbins=100,
title='Распределение зарплат c учетом графика работы',
labels={'mean_salary': 'Зарплата', 'schedule.name': 'График'}
)
px.histogram(
df[df.mean_salary < 500_000],
x='mean_salary',
color='salary.currency',
nbins=100,
title='Распределение зарплат c учетом валюты работы',
labels={'mean_salary': 'Зарплата', 'salary.currency': 'Валюта'}
)
px.histogram(
df,
x='salary.currency',
title='Количество вакансий для каждой валюты',
labels={'salary.currency': 'Валюта'}
).update_xaxes(categoryorder='total descending')
df_specs = df.copy()
df_specs.specializations = df_specs.specializations.map(lambda specs: list(map(itemgetter('name'), specs)))
df_specs = df_specs[df_specs.specializations.notna()]
df_specs['specialization_profarea_names'] = df.specializations.map(lambda specs: list(set(map(itemgetter('profarea_name'), specs))))
df_specs = df_specs[df_specs.specialization_profarea_names.notna()]
df_specs[['specialization_profarea_names', 'specializations']].head(10)
| specialization_profarea_names | specializations | |
|---|---|---|
| _id | ||
| 49810439 | [Транспорт, логистика] | [Автоперевозки, Водитель, Логистика, Экспедитор] |
| 49810551 | [Административный персонал, Домашний персонал] | [Уборщица/уборщик, домработница/домработник, Г... |
| 49810468 | [Спортивные клубы, фитнес, салоны красоты] | [Парикмахер] |
| 45788942 | [Транспорт, логистика] | [Кладовщик, Рабочий склада, Логистика] |
| 49810601 | [Банки, инвестиции, лизинг, Бухгалтерия, управ... | [Учет заработной платы, Основные средства, Нал... |
| 49810507 | [Административный персонал] | [Управляющий офисом (Оffice manager), Персонал... |
| 49810469 | [Спортивные клубы, фитнес, салоны красоты] | [Парикмахер] |
| 49810426 | [Спортивные клубы, фитнес, салоны красоты] | [Массажист] |
| 47003369 | [Продажи] | [Прямые продажи, Менеджер по работе с клиентами] |
| 43592367 | [Бухгалтерия, управленческий учет, финансы пре... | [Аудит, Другое, Финансовый анализ] |
all_specializations = list(reduce(set.union, df_specs.specializations, set()))
all_specializations[:15]
['Мебельное производство', 'CRM системы', 'Акции, Ценные бумаги', 'Железнодорожные перевозки', 'Оптимизация сайта (SEO)', 'Кассир, Инкассатор', 'Информационные технологии, Интернет, Мультимедиа', 'Управляющий офисом (Оffice manager)', 'Арт-директор', 'Секретарь', 'Машинист производства', 'Закупки и снабжение', 'Диспетчер', 'Персональный ассистент', 'Компьютерная безопасность']
len(all_specializations)
504
count_by_specialization = {spec: df_specs.specializations.map({spec}.issubset).sum() for spec in all_specializations}
count_by_specialization = Counter(count_by_specialization)
count_by_specialization.most_common(10)
[('Розничная торговля', 10704),
('Начальный уровень, Мало опыта', 9135),
('Торговые сети', 8017),
('Продукты питания', 7397),
('Продажи', 6681),
('Продавец в магазине', 6055),
('Менеджер по работе с клиентами', 5314),
('Прямые продажи', 4366),
('Другое', 3127),
('Водитель', 2872)]
all_profareas = reduce(set.union, df_specs.specialization_profarea_names, set())
len(all_profareas)
28
{profarea: df_specs[df_specs.specialization_profarea_names.map({profarea}.issubset)]['mean_salary'].mean() for profarea in all_profareas}
{'Административный персонал': 48667.45219370861,
'Строительство, недвижимость': 84771.85741049125,
'Туризм, гостиницы, рестораны': 47756.217606707316,
'Информационные технологии, интернет, телеком': 92008.00538176925,
'Инсталляция и сервис': 61575.194444444445,
'Искусство, развлечения, масс-медиа': 56821.15873015873,
'Высший менеджмент': 123710.24584717608,
'Автомобильный бизнес': 72177.20721925133,
'Производство, сельское хозяйство': 64457.76760048721,
'Управление персоналом, тренинги': 65502.94584382872,
'Транспорт, логистика': 67427.33646295663,
'Бухгалтерия, управленческий учет, финансы предприятия': 48283.09805153991,
'Юристы': 55641.35897435898,
'Наука, образование': 45231.578881987574,
'Государственная служба, некоммерческие организации': 52380.44400785855,
'Добыча сырья': 94832.55590062111,
'Безопасность': 53167.152825836216,
'Спортивные клубы, фитнес, салоны красоты': 51376.46395250212,
'Рабочий персонал': 64773.36851851852,
'Медицина, фармацевтика': 55659.44958753437,
'Маркетинг, реклама, PR': 60350.047784967646,
'Консультирование': 92617.73353751915,
'Страхование': 72418.31764705882,
'Закупки': 62442.23834886817,
'Банки, инвестиции, лизинг': 57242.99230111206,
'Домашний персонал': 41872.405241935485,
'Продажи': 49176.52389049481,
'Начало карьеры, студенты': 43474.821203244705}
df_profarea = pd.DataFrame({
profarea: df_specs[
df_specs.specialization_profarea_names
.map({profarea}.issubset)
]['mean_salary'].agg(['mean', 'sum', 'count'])
for profarea in all_profareas
}).T
df_profarea = df_profarea.astype(np.int64).reset_index().rename(columns={'index': 'profarea'})
df_profarea.head(10)
| profarea | mean | sum | count | |
|---|---|---|---|---|
| 0 | Административный персонал | 48667 | 235161129 | 4832 |
| 1 | Строительство, недвижимость | 84771 | 407244003 | 4804 |
| 2 | Туризм, гостиницы, рестораны | 47756 | 125312315 | 2624 |
| 3 | Информационные технологии, интернет, телеком | 92008 | 273539800 | 2973 |
| 4 | Инсталляция и сервис | 61575 | 28817191 | 468 |
| 5 | Искусство, развлечения, масс-медиа | 56821 | 39377063 | 693 |
| 6 | Высший менеджмент | 123710 | 74473568 | 602 |
| 7 | Автомобильный бизнес | 72177 | 107977102 | 1496 |
| 8 | Производство, сельское хозяйство | 64457 | 264599136 | 4105 |
| 9 | Управление персоналом, тренинги | 65502 | 52009339 | 794 |
px.bar(
df_profarea,
x='profarea',
y='count',
labels={'profarea': 'Профобласть', 'count': 'Количество вакансий'},
text_auto='.2s',
title='Количество вакансий в каждой области'
).update_xaxes(categoryorder='total descending')
px.pie(
df_profarea,
names='profarea',
values='count',
labels={'index': 'Профобласть', 'count': 'Количество вакансий'},
title='Доля вакансий для каждой области'
)
px.bar(
df_profarea,
x='profarea',
y='mean',
labels={'profarea': 'Профобласть', 'mean': 'Средняя зарплата'},
text_auto='.2s',
title='Средняя зарплата в каждой области'
).update_xaxes(categoryorder='total descending')
px.bar(
df_profarea,
x='profarea',
y='sum',
labels={'profarea': 'Профобласть', 'sum': 'Сумма всех зарплат'},
text_auto='.2s',
title='Сумма зарплат в каждой области'
).update_xaxes(categoryorder='total descending')